package MesaCircLpm;

// ----------------------------------------------------------------
//
// The LPM module is responsible for taking 32-bit IP addresses, looking up
// the destination (32-bit data) for each IP address in a table in an SRAM,
// and returning the destinations.
//
// ----------------------------------------------------------------
//
// ILpm is the interface to the LPM module, containing two
// sub-interfaces: mif and ram.  It is defined in the interface definition
// package, MesaIDefs, imported above.
//
// ----------------------------------------------------------------
//
// The LPM module sits between two other blocks:
//   * the MIF (Media Interface)
//   * the SRAM
//
// The LPM receives requests from the MIF module by the method
//     mif.put (LuRequest, LuTag)
// and returns results (some cycles later) of the form (luResponse, luTag) by
// the method
//     mif.get.
//
// The LPM sends addresses to the RAM by calling
//     ram.put(sramAddr)
// and receives result data (some cycles later) from the RAM by the method
//     ram.get
//
// ----------------------------------------------------------------
//
// The longest prefix match traverses a tree representation in  memory. The
// first step is a table lookup, and after that it iterates if necessary until
// a leaf is reached.
//
// ----------------------------------------------------------------
//
// The module is pipelined, i.e., IP addresses stream in, and results stream
// out (after some latency).  Results are returned in the same order as
// requests.
//
// The SRAM is also pipelined: addresses stream in, and data stream out
// (after some latency).  It can accept addresses and deliver data on every
// cycle.
//
// Performance metric: as long as IP lookup requests are available, the LPM
// module must keep the SRAM 100% utilized, i.e., it should issue a request
// into the SRAM on every cycle.
//
// ----------------------------------------------------------------
//
// This version uses a circular pipeline, with one memory access each cycle
// for each circulating value.
//
// ----------------------------------------------------------------

// Required library packages:
import FIFO::*;
import RAM::*;
import ClientServer::*;
import CompletionBuffer::*;
import GetPut::*;
import CGetPut::*;
import List::*;
import Connectable::*;

// Other Mesa packages:
import MesaDefs::*;
import MesaIDefs::*;

// The type of results from the table lookup:

typedef union tagged {
    Bit#(21) Pointer;
    Bit#(31) Leaf;
} TableType deriving (Eq, Bits);

// The first lookup consumes the first 16 bits of the IP address.  The
// following type is for the remainder -- either two eight-bit chunks (after
// the first lookup) or just one (after the second):

typedef union tagged {
    Tuple2#(Bit#(8),Bit#(8)) L1;
    Bit#(8) L2;
    void L3;
} IP deriving (Eq, Bits);

// Latency is the total latency for the Lpm; this includes all steps in
// the process and the memory latency.  If latency is too low then the
// external ram will not be fully utilized. If the latency is too high
// then unnecessarily large FIFOs are created.

typedef 15 Latency;

// Longest prefix matching is done on several requests at the same time. A
// completion buffer ensures that the output is in the same order as the
// requests. The completion buffer also limits the number of requests worked
// on at the same time.

typedef Latency CompletionBufferSize;
typedef CBToken#(CompletionBufferSize) CompletionToken;

(* synthesize *)
module mkMesaLpm(ILpm);
   // registers for debugging purposes:
   Reg#(LuRequest) requestB32();
   mkRegU the_requestB32(requestB32);

   Reg#(LuTag) requestTag();
   mkRegU the_requestTag(requestTag);

   Reg#(LuResponse) responseB32();
   mkRegU the_responseB32(responseB32);

   Reg#(LuTag) responseTag();
   mkRegU the_responseTag(responseTag);

   // Technical detail: Latency is actually a numeric type.  We now define
   // an integer with corresponding value:
   Integer sz = valueOf(Latency);

   // the FIFOs for requests to and responses from the RAM:
   FIFO#(SramAddr) sramReq();
   mkSizedFIFO#(2) the_sramReq(sramReq);

   FIFO#(SramData) sramResp();
   mkSizedFIFO#(sz + 2) the_sramResp(sramResp);

   // The FIFO for input requests:
   FIFO#(Tuple2#(LuRequest, LuTag)) ififo();
   mkSizedFIFO#(sz) the_ififo(ififo);

   // The FIFO for work in progress:
   FIFO#(Tuple3#(IP, LuTag, CompletionToken)) fifo();
   mkSizedFIFO#(sz + 2) the_fifo(fifo);

   // The Completion Buffer, which holds completed results
   CompletionBuffer#(CompletionBufferSize, Tuple2#(LuResponse, LuTag)) completionBuffer();
   mkCompletionBuffer the_completionBuffer(completionBuffer);

   // A FIFO for completion tokens from the Completion Buffer:
   FIFO#(CompletionToken) tagfifo();
   mkSizedFIFO#(sz+2) the_tagfifo(tagfifo);

   // populate the tagfifo:
   rule get_tags;
      let tag <- completionBuffer.reserve.get;
      tagfifo.enq(tag);
   endrule

   // All processing starts with a table lookup. This rule also claims a
   // completion token from the completion buffer:
   rule stage0;
      match {.ireq,.ilutag} = ififo.first;
      ififo.deq;
      let tag = tagfifo.first;
      tagfifo.deq;

      // We send a request to the RAM, and also enqueue (for the next
      // stage) the remainder of the address, the lookup-tag and the
      // completion-buffer tag.  (Ln is the remainder after the nth
      // lookup).
      fifo.enq(tuple3(L1 (tuple2(ireq[15:8], ireq[7:0])), ilutag, tag));
      sramReq.enq(zeroExtend(ireq[31:16]));
   endrule: stage0

   // Processing stops if we found a Leaf.
   rule stage1_Leaf (unpack(sramResp.first) matches tagged Leaf (.v));
      let {ip,lutag,itag} = fifo.first;
      completionBuffer.complete.put(tuple2(itag,tuple2(unpack({0,v}),lutag)));
      // TASK: complete the definition of this rule.
   endrule: stage1_Leaf

   // If we found a pointer, a further lookup is necessary.  We also
   // enqueue the remainder of the address (Ln is the remainder after the
   // nth lookup).
   rule stage1_Pointer (unpack(sramResp.first) matches tagged Pointer (.p));
      sramResp.deq;
      let {ip,lutag,itag} = fifo.first;
      fifo.deq;
      // nr: next remainder
      let nr = begin case (ip) matches
			tagged L1 {.b,.c} :  (L2 (c));
			tagged L2 {.c}    :  (L3);
			tagged L3         :  (?);  // never happens
		     endcase end;
      let addr = ?; // TASK: complete this definition.

      fifo.enq(tuple3(nr,lutag,itag));
      sramReq.enq(addr);
   endrule: stage1_Pointer

   // Finally we define the two interfaces:
   interface Server mif;
      interface Put request;
	 method put(x);
	    action
	       ififo.enq(x);
	       // record request for debugging purposes:
	       let {ipa,lutag} = x;
	       requestB32 <= ipa;
	       requestTag <= lutag;
	    endaction
	 endmethod: put
	 // or, apart from the debugging info:
	 // interface request = fifoToPut(ififo);
      endinterface: request
      interface Get  response;
	 method get() ;
	    actionvalue
	       let resp <- completionBuffer.drain.get();
	       // record response for debugging purposes:
	       let {r,t} = resp;
	       responseB32 <= r;
	       responseTag <= t;
	       return(resp);
	    endactionvalue
	 endmethod: get
	 // or, apart from the debugging info:
	 // interface response = fifoToGet(completionBuffer.drain);
      endinterface: response
   endinterface: mif

   interface Client ram;
      interface Get request;
	 method get();
	    actionvalue
	       let req = sramReq.first;
	       sramReq.deq;
	       return (Read (req));
	    endactionvalue
	 endmethod: get
      endinterface: request
      interface Put response = fifoToPut(sramResp);
   endinterface: ram
endmodule

endpackage

/*
 TASK:
 When you have got this version to work, investigate how its performance is
 affected by the value of latency above.

 Consider, too, how the quality of the design might be affected by the precise
 details of the definition of the type IP.
*/
